﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Chip8Dev.CPU
{

    public delegate void OpcodeCall(Instruction inst);

    public sealed class InstructionFetcher
    {
        private Chip8Engine m_Engine;
        private Dictionary<Int32, Instruction> m_InstructionCache;

        public InstructionFetcher(Chip8Engine engine)
        {
            m_Engine = engine;
        }

        public void FetchNextInstruction()
        {
            Instruction inst = null;

            /* Read Memory */
            Int32 read = m_Engine.Memory.ReadInt16(m_Engine.CPUCoreState.PC);

            /* Fetch the instruction */
            if (m_InstructionCache.ContainsKey(m_Engine.CPUCoreState.PC))
            {
                /* Check if the instruction is the same */
                if (read == m_InstructionCache[m_Engine.CPUCoreState.PC].InstructionBytes)
                {
                    m_InstructionCache.TryGetValue(m_Engine.CPUCoreState.PC, out inst);
                }
                else
                {
                    /* Create new instruction */
                    inst = new Instruction(DecodeOpcode(read), read);

                    /* Update the cache */
                    m_InstructionCache[m_Engine.CPUCoreState.PC] = inst;
                }
            }
            else
            {
                /* Create new instruction */
                inst = new Instruction(DecodeOpcode(read), read);

                /* Stick it in the cache */
                m_InstructionCache.Add(m_Engine.CPUCoreState.PC, inst);
            }

            TranslateToCall(inst);
            m_Engine.CPUCoreState.PC += 2;
        }

        private void TranslateToCall(Instruction inst)
        {
            OpcodeCall callee = OpcodeExecute;

            if (callee != null)
            {
                OpcodeExecute(inst);
            }
        }

        public NamedOpcode DecodeOpcode(Int32 inst)
        {
            if (inst == 0)
            {
                return NamedOpcode.NOP;
            }

            Int32 mask = 0xF000;
            Int32 opcode = ((inst & mask) >> 12);

            switch (opcode)
            {
                    case 0x0: mask = ((inst & 0xFFF0) == 0x00C0) ? 0xFFF0 : 0xFFFF; break;
                    case 0xF:
                    case 0xE: mask = 0xF0FF; break;
                    case 0x8: mask = 0xF00F; break;
                    default: break;
            }

            switch (inst & mask)
            {
                case 0x00E0: return NamedOpcode.CLS;
                case 0x00EE: return NamedOpcode.RET;
                case 0x1000: return NamedOpcode.JP_1;
                case 0x2000: return NamedOpcode.CALL;
                case 0x3000: return NamedOpcode.SE_3;
                case 0x4000: return NamedOpcode.SNE_4;
                case 0x5000: return NamedOpcode.SE_5;
                case 0x6000: return NamedOpcode.LD_6;
                case 0x7000: return NamedOpcode.ADD_7;
                case 0x8000: return NamedOpcode.LD_8;
                case 0x8001: return NamedOpcode.OR;
                case 0x8002: return NamedOpcode.AND;
                case 0x8003: return NamedOpcode.XOR;
                case 0x8004: return NamedOpcode.ADD_8;
                case 0x8005: return NamedOpcode.SUB;
                case 0x8006: return NamedOpcode.SHR;
                case 0x8007: return NamedOpcode.SUBN;
                case 0x800E: return NamedOpcode.SHL;
                case 0x9000: return NamedOpcode.SNE_9;
                case 0xA000: return NamedOpcode.LD_A;
                case 0xB000: return NamedOpcode.JP_B;
                case 0xC000: return NamedOpcode.RND;
                case 0xD000: return NamedOpcode.DRW;
                case 0xE09E: return NamedOpcode.SKP;
                case 0xE0A1: return NamedOpcode.SKNP;
                case 0xF007: return NamedOpcode.LD_F07;
                case 0xF00A: return NamedOpcode.LD_F0A;
                case 0xF015: return NamedOpcode.LD_F15;
                case 0xF018: return NamedOpcode.LD_F18;
                case 0xF01E: return NamedOpcode.LD_F1E;
                case 0xF029: return NamedOpcode.LD_F29;
                case 0xF033: return NamedOpcode.LD_F33;
                case 0xF055: return NamedOpcode.LD_F55;
                case 0xF065: return NamedOpcode.LD_F65;
                default: return NamedOpcode.NOP;
            }
        }

        /* Opcode Events */
        public event OpcodeCall OpcodeExecute;
    }
}
